#-*- coding: utf8 -*-
import operator
import functools
import collections
import itertools
import numpy as np
from cStringIO import StringIO

__all__ = [ 'TimeSeries', 'TimeSeriesCS' ]

class TimeSeries(collections.Sequence):
    def __init__(self, times, data, copy=None, copy_times=None, copy_data=None, names=None):
        '''
        Represents time series data.
        
        Parameters
        ---------
         
        times : Sequence
            Times associated to each data point
        data : Sequence
            A sequence of data points. Must be of the same size of times.

        Optional parameters
        -------------------
        
        copy_times, copy_data : bool
            If False and times and data are ndarrays, it avoids copying data 
            from the times/data parameters to the times/data sections of the
            time series. In this case, any change to the original times/data
            objects will propagate to the TimeSeries object. The default 
            behavior is to copy if times and data are writable arrays.
        copy : bool
            Set the value of copy_times and copy_data simultaneously

        Attributes
        ----------
        
        times, data : arrays
            Arrays equal to the input times and data parameters.
        data_dim : int
            Dimension of data points.
        delta_t, time_deltas : arrays
            Arrays holding time differences between each data point. 'delta_t'
            differs from 'time_deltas' for time series with constant sampling
            rate, in which the first is a number and the second is always an 
            array. 

        Mathematical operations
        -----------------------
        
        Mathematical operations in TimeSeries objects happens in the data 
        section of the object and leave the times intact. TimeSeries support
        the basic mathematical operations and Numpy's ufuncs. Only time series
        with equal times can be manipulated together.
        
        >>> import numpy as np
        >>> ts_1 = TimeSeries(range(5), [1, 2, 1, 2, 1])
        >>> ts_2 = TimeSeries(range(5), [0, 1, 2, 1, 0])
        >>> (ts_1 + ts_2).data
        array([1, 3, 3, 3, 1])
        
        Notes
        -----
        
        There are several different classes that implement time series in more
        specialized contexts. See e.g. TimeSeriesCS or TimeSeriesDates. 
        
        Whenever it is necessary, times are assumed to be defined in seconds
        (which is the SI unit for time). The times section of all time series
        objects is immutable, but the data section (usually) can be manipulated.  
        Whenever one needs to change the times section, a new TimeSeries object
        must be created.
        
        The data points can be multidimensional and of any numeric type.
        '''
        if len(times) != len(data):
            raise ValueError('shape mismatch: times and data sections have different lengths')
        if copy is not None:
            copy_times = copy_data = copy
        self._set_times(times, copy_times)
        self._set_data(data, copy_data)
        if names is not None:
            if len(names) == self.data.shape[1]:
                self._names = tuple(names)
            else:
                raise ValueError("'names' must be of the same size as each data point")

    def _set_times(self, times, copy=None):
        if copy is None:
            try:
                copy = times.flags.writable
            except AttributeError:
                copy = True
        self.times = np.array(times, copy=copy)
        self.times.flags.writeable = False

    def _set_data(self, data, copy=None):
        if copy is None:
            try:
                copy = data.flags.writable
            except AttributeError:
                copy = True
        self.data = np.array(data, copy=None)
        self.data.flags.writeable = False

        # Compute dimensions/shape and check if arrays are aligned
        shape = list(self.data.shape)
        shape.pop(0)
        self.data_shape = tuple(shape)
        self.data_dim = reduce(lambda x, y: x * y, [1] + shape)

    # Mathematical operations --------------------------------------------------
    def _get_compatible_data(self, other):
        '''
        Internal use. Assure that 'other' is mathematically compatible with self,
        i.e., if it is a time series, it must have the same time section.
        
        Return a array or numeric element that can be used in mathematical 
        expressions with the data section of self.  
        '''

        try:
            if len(self) != len(other):
                raise ValueError('time series must be of equal length')
        except TypeError:
            pass # othe is a number

        if isinstance(other, TimeSeries):
            if any(self.times != other.times):
                raise ValueError('times section mismatch')
            else:
                return other.data
        else:
            return other

    # Addition
    def __add__(self, other):
        other = self._get_compatible_data(other)
        new_data = self.data + other
        return self.copy(data=new_data)

    def __radd__(self, other):
        return self + other

    def __iadd__(self, other):
        other = self._get_compatible_data(other)

        # Temporarely set writeable flag to True and write to the data section 
        try:
            self.data.flags.writeable = True
            self.data += other
        finally:
            self.data.flags.writeable = False
        return self

    # Subtraction
    def __sub__(self, other):
        other = self._get_compatible_data(other)
        new_data = self.data - other
        return self.copy(data=new_data)

    def __rsub__(self, other):
        other = self._get_compatible_data(other)
        new_data = other - self.data
        return self.copy(data=new_data)

    def __isub__(self, other):
        other = self._get_compatible_data(other)

        # Temporarely set writeable flag to True and write to the data section 
        try:
            self.data.flags.writeable = True
            self.data -= other
        finally:
            self.data.flags.writeable = False
        return self

    # Multiplication
    def __mul__(self, other):
        other = self._get_compatible_data(other)
        new_data = self.data * other
        return self.copy(data=new_data)

    def __rmul__(self, other):
        other = self._get_compatible_data(other)
        new_data = other * self.data
        return self.copy(data=new_data)

    def __imul__(self, other):
        other = self._get_compatible_data(other)

        # Temporarely set writeable flag to True and write to the data section 
        try:
            self.data.flags.writeable = True
            self.data *= other
        finally:
            self.data.flags.writeable = False
        return self

    # Division
    def __div__(self, other):
        other = self._get_compatible_data(other)
        new_data = self.data / other
        return self.copy(data=new_data)

    def __rdiv__(self, other):
        other = self._get_compatible_data(other)
        new_data = other / self.data
        return self.copy(data=new_data)

    def __idiv__(self, other):
        other = self._get_compatible_data(other)

        # Temporarely set writeable flag to True and write to the data section 
        try:
            self.data.flags.writeable = True
            self.data /= other
        finally:
            self.data.flags.writeable = False
        return self

    # Power
    def __pow__(self, other, z=None):
        other = self._get_compatible_data(other)
        new_data = pow(self.data, other, z)
        return self.copy(data=new_data)

    def __ipow__(self, other,):
        other = self._get_compatible_data(other)

        # Temporarely set writeable flag to True and write to the data section 
        try:
            self.data.flags.writeable = True
            self.data **= other
        finally:
            self.data.flags.writeable = False
        return self

    # Unary arithmetic
    def __pos__(self):
        return self.copy(data= +self.data)

    def __neg__(self):
        return self.copy(data= -self.data)

    # Comparação
    def __eq__(self, other):
        return (self.times == other.times).all() and (self.data == other.data).all()


    # Get and iterate over times and data sections of time series --------------
    def items(self, reversed=False):
        '''
        Iterator that returns a tuple with (time, data_point) for each point
        in the time series.
        
        Parameters
        ----------
        
        reversed : bool
            If True, iterate over the time series backwards.
        '''

        if reversed:
            idx = len(self) - 1
            T, X = self.times, self.data
            while idx >= 0:
                yield (T[idx], X[idx])
                idx -= 1
        else:
            for item in itertools.izip(self.times, self.data):
                yield item

    def get_item(self, idx):
        'Return a tuple with (time, data_point) at index idx.'
        return (self.times[idx], self.data[idx])

    def get_time(self, idx):
        "Get time at index 'idx'."
        return self.times[idx]

    # Sequence operations ------------------------------------------------------
    # All sequence methods operate in the data section of the array, except
    # slicing operations which return new TimeSeries objects
    def __iter__(self):
        return iter(self.data)

    def __getitem__(self, idx):
        'ts[idx] <==> ts.data[idx]'
        return self.data[idx]

    def __getslice__(self, i, j):
        return self.copy(data=self.data[i:j], times=self.times[i:j])

    def __setitem__(self, idx, value):
        try:
            self.data.flags.writeable = True
            self.data[idx] = value
        finally:
            self.data.flags.writeable = False

    def __len__(self):
        return len(self.times)

    # Ufuncs support -----------------------------------------------------------
    def __array_priority__(self):
        '''
        Array priority is 20.0, and it is higher than matrices and all ndarray
        subtypes.
        '''
        return 20.0

#    def __array_prepare__(self, array, context=None):
#        print 'array_prepare'
#        print array
#        print context
#        return array

    def __array_wrap__(self, array, context=None):
        _ufunc, ts_obj, _idx = context
        return ts_obj[0].copy(data=array)


    # Other useful methods -----------------------------------------------------
    def copy(self, times=None, data=None, names=None):
        '''
        Return a copy of the time series object replacing the times or data
        sections by the given values, if specified.
        '''
        new = object.__new__(type(self))

        # Save times section
        if times is None:
            if isinstance(self, TimeSeriesCS):
                pass
            else:
                new.times = self.times
        else:
            new._set_times(times)

        # Save data section
        if data is None:
            new.data = np.array(self.data, copy=True)
            new.data_dim = self.data_dim
            new.data_shape = self.data_shape
        else:
            new._set_data(data)

        # Save names
        if names is None:
            try:
                new._names = self._names
            except AttributeError:
                pass

        return new

    # Query methods ------------------------------------------------------------
    def is_chronological(self):
        'Return True if time series is in chronological order.'
        # Check if any time difference is zero
        return not any(t1 < t0 for (t1, t0) in itertools.izip(self.times[1:], self.times))

    def is_function(self):
        '''Return True if time series can be seen as a function of time, i.e., 
        if there are no two data points associated with the same time. 
        '''
        return len(set(self.times)) == len(self.times)

    def is_increasing(self):
        '''Return True if all dimensions of all data points are strictly 
        increasing functions of time. 
        '''
        ordered = self.make_chronological()
        data_diff = np.diff(ordered.data)
        return (data_diff.flat > 0).all()

    def is_decreasing(self):
        '''Return True if all dimensions of all data points are strictly 
        decreasing functions of time. 
        '''
        ordered = self.make_chronological()
        data_diff = np.diff(ordered.data)
        return (data_diff.flat < 0).all()

    def is_nonincreasing(self):
        '''Return True if all dimensions of all data points are decreasing 
        functions of time. 
        '''
        ordered = self.make_chronological()
        data_diff = np.diff(ordered.data)
        return (data_diff.flat <= 0).all()

    def is_nondecreasing(self):
        '''Return True if all dimensions of all data points are increasing
        functions of time. 
        '''
        ordered = self.make_chronological()
        data_diff = np.diff(ordered.data)
        return (data_diff.flat >= 0).all()

    def is_monotonic(self):
        '''Return True if all dimensions of all data points are monotonic
        functions of time. 
        '''
        return self.is_nonincreasing() or self.is_nondecreasing()

    def is_strict_monotonic(self):
        '''Return True if all dimensions of all data points are strictly 
        monotonic functions of time. 
        '''
        return self.is_increasing() or self.is_decreasing()

    def is_positive(self):
        '''Return True if all data values are greater than or equal to zero.'''

        return (self.data.flat >= 0).all()

    def is_negative(self):
        '''Return True if all data values are less than or equal to zero.'''
        return (self.data.flat <= 0).all()

    def is_nonzero(self):
        '''Return False if any value is equal to zero.'''
        return (self.data.flat != 0).all()

    # Data permanence ----------------------------------------------------------
    def to_string(self):
        '''
        Serializes the TimeSeries object to a string. The object can be
        recovered using the TimeSeries.from_string(st) function.    
        '''

        file = StringIO()
        try:
            self.to_file(file)
            st = file.getvalue()
        finally:
            file.close()
        return st

    @classmethod
    def from_string(cls, st):
        '''
        Creates a new TimeSeries object from string 'st'.
        
        Example
-        -------
        
        >>> ts = TimeSeries([1,2,3], [3,2,1])
        >>> data_st = ts.to_string()
        >>> ts == TimeSeries.from_string(data_st)
        True
        '''
        file = StringIO(st)
        try:
            new = cls.from_file(file)
        finally:
            file.close()
        return new

    def to_file(self, file):
        '''
        Serializes the TimeSeries object to a file object. The object can be
        recovered using the TimeSeries.from_file(st) function.    
        '''
        #FIXME: Use pickle for now. But we want a more efficient alternative later
        import cPickle as pickle
        pickle.dump(self, file)

    @classmethod
    def from_file(cls, file):
        '''
        Creates a new TimeSeries object from file object 'file'.
        '''
        import cPickle as pickle
        return pickle.load(file)

    # Views and transformation of time series ----------------------------------
    def view_dim(self, idx):
        '''
        For multidimensional time series, return a TimeSeries object with an 
        one dimensional view of the idx-th component of data. If idx is not an
        integer, it is interpreted as the name of the given dimension of data. 
        '''
        if not isinstance(idx, int):
            try:
                idx = self._names.index(idx)
            except ValueError:
                raise ValueError("invalid name for dimension, '%s'" % idx)
            except AttributeError:
                pass
        return self.copy(data=self.data[:, idx])

    def make_chronological(self):
        '''
        Return a chronologically ordered version of time series. 
        '''
        if self.is_chronological():
            return self
        else:
            t = list(zip(self.times, self.data))
            t.sort(key=lambda (t, x): t)
            data = [y for (x, y) in t]
            times = [x for (x, y) in t]
            return TimeSeries(times, data)

    def make_function(self, epsilon=1e-10):
        '''
        Return a new time series that surely represents a function , i.e.,
        there are no data points assigned to the same instant of time. 
        
        If exists two or more values associated with the same time argument in 
        the original time series, each time value is displaced by a quantity 
        epsilon > 0 that ensures that the new time series is a function. 
        '''

        # The code requires that the time series is chronological
        if not self.is_chronological():
            new = self.make_chronological()
            return new.make_function(epsilon)

        times = list(np.copy(self.times))
        for i in xrange(1, len(times)):
            if times[i] <= times[i - 1]:
                times[i] = times[i - 1] + epsilon

        return self.copy(times=times)

    def resample(self, delta_t=None, numpoints=None, method='linear'):
        '''
        Re-sample time series using one of the specified methods.
        
        Parameters
        ----------
        delta_t : float
            Time separation between consecutive points in the new time series.
        numpoints : int    
            Number of points in the new time series. Cannot be specified together
            with delta_t.
        method : str
            Specify the resampling method: 
             - 'back': uses the value of previous data point  
             - 'fwd': uses the value of the next data point
             - 'linear': linear interpolation between the neighboring data points 
             - 'quad'/'cubic': higher order interpolations
            
        Output
        ------
        
        A TimeSeriesCS object built with the new sampling rate.
        '''
        if delta_t is None and numpoints is None:
            raise ValueError("one of 'delta_t' or 'numpoints' must be specified")

        tf, t0 = self.t0, self.tf
        if numpoints is None:
            numpoints = int(np.abs(tf - t0) / delta_t + 1)

        # Use numpy.interp to resample using linear interpolation
        if method == 'linear':
            new_times = np.linspace(t0, tf, numpoints)
            new_data = np.interp(new_times, self.times, self.data)
            return TimeSeriesCS((t0, tf), new_data, names=self.names)

        # Transform a series to a piecewise constant function before using 
        # interpolation. 
        elif method == 'back':
            ts_pwc = pwc(self)
            new_times = np.linspace(t0, tf, numpoints)
            new_data = np.interp(new_times, ts_pwc.times, ts_pwc.data)
            return TimeSeriesCS((t0, tf), new_data, names=self.names)

        # Invert time series data and use method=back in the inverted series 
        elif method == 'fwd':
            new_series = self.copy(data=self.data[::-1])
            new_series = new_series.resample(numpoints=numpoints, method='back')
            return TimeSeriesCS((t0, tf), new_series.data[::-1])
        else:
            raise NotImplementedError

    def _resample_quad(self, delta_t):
        '''
        Re-sample time series using a linear interpolation between consecutive 
        points in order to estimate the data point in each time step.
        '''

        # Create list of times
        t0, tf = self.times[0], self.times[-1]
        times = np.arange(t0, tf, delta_t)
        data = np.empty(len(times))
        N = (tf - t0) / delta_t


        l = 0
        k = 0
        j = 0
        i = 0
        f = 3
        prodk = 1
        somk = 0
        tpt = 0

        for idx, t in enumerate(times):

            #início do método
            if tpt >= i: #and tpt <= f:
                print("Se t estiver entre i e f")
                while k <= f and k >= i:
                    print("Se tk tiver entre i e f")
                    tk = self.times[k]
                    while j <= f: #and j >=i:
                        tj = self.times[j]
                        if tpt != tj and tpt != tk and tk != tj:
                            prod = float((t - tj)) / float((tk - tj))
                            print("prod parcial")
                            print(prod)
                            prodk = prod * prodk
                            j += 1
                        prodk = prodk * self.data[k]
                        somk = somk + prodk
                        if tpt == tj:
                            print("Passou pelo t = tj")
                            prodk = 0
                            somk = self.data[j]
                            cont += 1
                    k += 1
                if cont == 4:
                    print("Passou pelo contador igual a 4")
                    somk = 0
                    prodk = 1
                    i = f
                    f = f + 3
            tpt += 1
            #final do método
            data[l] = somk
            l += 1
        self.data = data
        self.times = times
        return self.data

    # Properties ---------------------------------------------------------------
    @property
    def delta_t(self):
        return np.diff(self.times)

    @property
    def time_deltas(self):
        return np.diff(self.times)

    @property
    def t0(self):
        return self.times.min()

    @property
    def tf(self):
        return self.times.max()

    @property
    def time_range(self):
        return (self.t0, self.tf)

    @property
    def names(self):
        try:
            return self._names
        except AttributeError:
            return None

class TimeSeriesCS(TimeSeries):
    def __init__(self, delta_t, data, copy=None, copy_times=None,
                       copy_data=None, names=None, t0=None, tf=None):
        '''
        Represents a time series with constant sampling equally spaced time 
        intervals given by 'delta_t'.
        
        Parameters
        ---------
         
        delta_t : float or sequence
            The first argument may come in many different forms. If  delta_t is 
            a number, it is interpreted as the displacement between two 
            consecutive times. If it is a pair (t0, tf), it is interpreted as 
            the initial and final times of the time series. It can also be a 
            sequence of times of the same size as 'data'. If this sequence is 
            not uniformly increasing, a ValueError is raised.
        data: sequence
            Sequence of data points.
        t0, tf : float
            The initial/end time for the time series.
        copy, copy_data : bool
            If False and data is a ndarray, it avoids copying data.
            In this case, any change to the original data object will propagate
            to the TimeSeriesCS object. The default behavior is to copy if 
            data is a writable array. 
        copy_times : bool
            Dummy argument, is only present for sake of compatibility with 
            TimeSeries().

        Attributes
        ----------
        
        times, data : arrays
            Arrays equal to the input times and data parameters.
        delta_t, initial_time, final_time : float
            Same as above.
            
        Note
        ----
        
        TimeSeriesCS objects can be initialized using either 'delta_t' and 
        't0' parameters or using 't0' and 'tf'. In
        either case, the missing parameter is calculated from the input.
        
        If you specify delta_t, the initial time is assumed to be zero.
        
        >>> ts = TimeSeriesCS(0.1, [0, 1, 2, 1, 0])
        >>> ts.times; ts.data
        array([ 0. ,  0.1,  0.2,  0.3,  0.4])
        array([0, 1, 2, 1, 0])
        
        One can also set the initial and final times.
        
        >>> ts = TimeSeriesCS((11, 15), [0, 1, 2, 1, 0])
        >>> ts.times; ts.data
        array([ 11.,  12.,  13.,  14.,  15.])
        array([0, 1, 2, 1, 0])
        
        The series can also be initialized from a sequence of times, if all
        time displacements are equal
        
        >>> ts = TimeSeriesCS([ 0.,  0.1,  0.2,  0.3,  0.4], [0, 1, 2, 1, 0])
        >>> ts.time_range == (0.0, 0.4)
        True
        
        An ValueError is raised if the time sequence does not satisfy this
        constraint
        
        >>> ts = TimeSeriesCS([ 0.,  0.1,  0.2,  0.3,  1.0], [0, 1, 2, 1, 0])
        Traceback (most recent call last):
        ...
        ValueError: list of times does not increases uniformly
        '''

        data_len = len(data)
        try:
            N = len(delta_t)

            # Check if delta_t is a tuple (t0, tf)
            if N == 2:
                if t0 is None and tf is None:
                    t0, tf = delta_t
                    delta_t = None
                else:
                    TypeError('t0 and tf were set twice')

            # Now delta_t is interpreted as a list of times. It checks if
            # all time differences are equal (considering numerical errors) 
            elif N == data_len:
                self._set_times(delta_t)
                delta_t = None
                t0, tf = self.time_range
            else:
                raise ValueError('times and data have different lengths')

        except TypeError:
            pass

        # Determine the initial and final times from the given arguments
        if tf is None:
            t0 = (t0 if t0 is not None else 0.0)
            delta_t = (delta_t if delta_t is not None else 1.0)
            tf = t0 + delta_t * (data_len - 1)
        elif delta_t is None:
            t0 = (t0 if t0 is not None else 0.0)
        elif t0 is None:
            t0 = tf - delta_t * (data_len - 1)
        else:
            raise ValueError("cannot specify 't0', 'tf', 'delta_t' simultaneously")

        self._t0 = t0
        self._tf = tf
        self._set_data(data, copy)


    # Properties ---------------------------------------------------------------
    @property
    def times(self):
        times = np.linspace(self.t0, self.tf, len(self.data))
        times.flags.writeable = False
        return times

    @property
    def delta_t(self):
        return float(self.tf - self.t0) / len(self.data)

    @property
    def t0(self):
        return self._t0

    @property
    def tf(self):
        return self._tf

    #---------------------------------------------------------------------------
    # Methods that cannot be directly inherited directly from TimeSeries
    def _set_times(self, times, copy=None):
        tdiff = np.diff(times)
        delta_t = tdiff.mean()
        if tdiff.std() / np.abs(delta_t) > 1e-8:
            raise ValueError('list of times does not increases uniformly')
        self._t0 = times[0]
        self._tf = times[-1]

    def copy(self, **kwds):
        '''
        Make a copy of the current time series.
        
        Similar to TimeSeries.copy, but if a times sequence is specified, it 
        returns a TimeSeries object rather than a TimeSeriesCS object if the 
        sequence of times does not increases uniformly.
        '''
        try:
            return super(TimeSeriesCS, self).copy(**kwds)
        except ValueError:
            times = kwds['times']
            data = kwds.get('data', self.data)
            names = kwds.get('names', self.names)
            return TimeSeries(times, data, names=names)

    #---------------------------------------------------------------------------
    # Optimize some TimeSeries methods
    def is_chronological(self):
        return self._tf > self._t0

    def is_function(self):
        return self._tf != self._t0


def ts_filter(ts, cond=None, filter_times=False, **kwds):
    '''
    Filter time series using condition 'cond'.
    
    Parameters
    ----------
    
    ts : TimeSeries
        Any TimeSeries object
    cond : callable
        Keep all data points for which cond(x) == True.
    filter_times : bool
        If filter_times is True, apply condition 'cond' to the times section
        of the time series.
    '''
    if filter_times:
        filter_t, filter_d = [cond], []
    else:
        filter_t, filter_d = [], []

    for key, value in kwds.items():
        aux = key.split('__')
        key, params = aux[0], aux[1:]


        # process params
        if params == ['gt']:
            filter = functools.partial(operator.lt, value)
        elif params == ['gte']:
            filter = functools.partial(operator.le, value)
        elif params == ['lt']:
            filter = functools.partial(operator.gt, value)
        elif params == ['lte']:
            filter = functools.partial(operator.ge, value)
        elif params == ['eq']:
            filter = functools.partial(operator.eq, value)
        elif params == ['ne']:
            filter = functools.partial(operator.ne, value)
        else:
            raise TypeError("invalid filter'%s'" % params[0])

        # assign params to filters
        if key == 'data':
            filter_d.append(filter)
        elif key == 'times':
            filter_t.append(filter)
        else:
            raise TypeError('invalid keyword argument %s' % key)

    # Filter data
    times, data = [], []
    for t, x in ts.items():
        if all(f(t) for f in filter_t) and all(f(x) for f in filter_d):
            times.append(t)
            data.append(x)
    return TimeSeries(times, data)

def pwc(ts, method='back'):
    '''
    Return a piecewise constant version of the time series.
    
    Returns a TimeSeries object that represents a piecewise constant function.
    This means that the value of f(t) between ts.times[i] and ts.times[i + 1] is 
    ts.data[i].
    
    Example
    -------
    
    >>> t = np.arange(3.); ts = TimeSeries(t, t**2)
    >>> ts_pwc = pwc(ts)
    >>> list(ts.items()); list(ts_pwc.items())
    [(0.0, 0.0), (1.0, 1.0), (2.0, 4.0)]
    [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (2.0, 1.0), (2.0, 4.0)]
    '''

    times, data = ts.times, ts.data
    new_times = np.empty(2 * len(times) - 1)
    new_data = np.empty(2 * len(data) - 1)
    new_times[::2] = times
    new_times[1::2] = times[1:]
    new_data[::2] = data
    new_data[1::2] = data[:-1]

    return ts.copy(data=new_data, times=new_times)

if __name__ == '__main__':
    import doctest
    doctest.testmod(optionflags=doctest.REPORT_ONLY_FIRST_FAILURE)

    import matplotlib.pyplot as plt
    from visualization import ts_plot

    t = np.arange(0, 10, 0.01)
    x = t ** 2
    ts = TimeSeries(t, x)
    sin_ts = np.sin(ts) * ts
    print sin_ts, type(sin_ts)
    ts_plot(sin_ts)
    ts_plot(ts)
    plt.show()

#    y = np.cos(t) * 1.1
#    d = np.empty((1000, 2), dtype=float)
#    d[:, 0] = x
#    d[:, 1] = y
#    ts = TimeSeries(t, d)
#    ts = TimeSeries(t, x)
#    ts = ts_filter(ts, data__lt=0.5)
#
#    ts_plot(ts)
#    plt.show()
#
#    print(ts)
